<?php
/**
 * SimpleDatexS.php
 * 
 * This file contains the SimpleDatexS class, a simple object representation of a table row.
 * @author Robert Schmidt <robert@wildpack.de>
 * @version 1.0
 * @package wildpack
 * @subpackage dataaccess.wpdatex
 * @dependencies: wildpack.base.observe.*
 */


/**
 * An ORM implementation for a single data row
 * 
 * This is a simple object representation of a table row. The class is declared as abstract. Methods containing 
 * data about the table must be implemented in extending classes. The SimpleDatexS class does only support tables 
 * with a single primary key field. The primary key is used to identify datasets. The class does neither exclusively 
 * supports the auto_increment feature, nor prevents using it. Values, created by the database are not exclusively 
 * reported back to the SimpleDatexS object. You should reload the object after saving when using this functionality.
 * 
 * How to extend this class:
 * <code>
 * class MyData extends SimpleDatexS
 * {
 * 		//Implement the abstract getTable method
 * 		public function getTable()
 * 		{
 * 			return 'MyTable';
 * 		}
 * 
 * 		//Implement the abstract getFieldType method
 * 		public function getFieldType($fieldname)
 * 		{
 * 			switch ($fieldname) {
 * 				case 'intfield' : return 'number';
 * 				case 'floatfield' : return 'number';
 * 				case 'textfield' : return 'string';
 * 			}
 * 		}
 * 
 * 		//Implement the abstract getFields method
 * 		public function getFields()
 * 		{
 * 			return array(
 * 				'intfield',
 * 				'floatfield',
 * 				'textfield'
 * 			);
 * 		}
 * 		
 * 		//Implement the abstract getPkField method
 * 		public function getPkField()
 * 		{
 * 			return 'intfield';
 * 		}
 * 
 * 		//Implement the abstract getVType method
 * 		public function getVType($fieldname)
 * 		{
 * 			switch ($fieldname) {
 * 				case 'intfield' : return 'not_null';
 * 				case 'textfield' : return 'string_not_empty';
 * 			}
 * 		}
 * }
 * </code>
 * 
 * How to use the extended class:
 * <code>
 * $con = new WPConnectMySQL(array(
 * 		'location' => 'localhost/mydb',
 * 		'user' => 'me',
 * 		'password' => 'my_password'
 * ));
 * 
 * //Create new dataset
 * $md = new MyData($con);
 * $md->set('intfield', 1);
 * $md->set('floatfield', 1.1);
 * $md->set('textfield', 'Lorem ipsum');
 * $md->save();
 * 
 * //Load dataset
 * $md = new MyData($con);
 * $md = $md->load(array(
 * 		'intfield' => 1
 * ));
 * 
 * //Validate before save
 * if ($md->validate() !== true) {
 * 		die('Fields did not pass validation check.');
 * }
 * 
 * //Update dataset
 * if (!is_null($md)) {
 * 		$md->set('textfield', 'Updated text');
 * 		$md->save();
 * }
 * 
 * //Remove dataset
 * $md->remove();
 * 
 * </code>
 * 
 * ATTENTION: Due to problems with float values (see: http://dev.mysql.com/doc/refman/5.0/en/problems-with-float.html) 
 * it is recommend to use the double datatype for columns especially if it is planned to use them in select 
 * statements.
 * 
 * @author Robert Schmidt <robert@wildpack.de>
 * @version 1.0
 * @package wildpack
 * @subpackage dataaccess.wpdatex
 * @dependencies: wildpack.base.observe.*
 * @abstract
 */
abstract class SimpleDatexS extends WpObservable
{
	/**
	 * @var string The error space to use for error logging
	 */
	public static $_espc = '22';
	
	/**
	 * @var bool This variable is for internal use and indicates wether the object represents a new dataset or not
	 */
	public $_isNew = true;
		
	/**
	 * @var The connector to use accessing the datasource
	 */
	public $Connector = null;
	
	/**
	 * @var The data as associtative array with fieldnames as keys
	 */
	public $Data = array();
	
	/**
	 * @var bool True if dataset has been loaded successfully, false else
	 */
	public $Loaded = false;
	
	/**
	 * @var object The log object to use for error logging
	 */
	public $Log = null;
	
	/**
	 * The class constructor. To quickly access or create data, it is possible to combine the constructor with 
	 * either loading data or filling fields of new data. Attention: Using actions, the constructor cannot return any
	 * status values of the action so failing methods are hard to track. Keep that in mind when using actions.
	 * 
	 * Example of use:
	 * <code>
	 * $conn = ... //Create a connector
	 * $log = ... //Create a log object
	 * 
	 * //Example1 - Simple constructor
	 * $sd = new SimpleDatexS($conn, $log);
	 * 
	 * //Example2 - Constructor loads dataset with specified id
	 * $sd = new SimpleDatexS($conn, $log, 'load', array('id' => 'an_id'));
	 * print($sd->get('id'));
	 * 
	 * //Example3 - Constructor prefills the the fields
	 * $sd = new SimpleDatexS($conn, $log, 'new', array('id' => 'an_id', 'textfield' => 'Lorem ipsum'));
	 * $sd->save();
	 * 
	 * //Example4 - Constructor updates a dataset. The primary key must be provided to identify the 
	 * $sd = new SimpleDatexS($conn, $log, 'update', array('id' => 'an_id', 'textfield' => 'Lorem ipsum'));
	 * 
	 * 
	 * </code>
	 * 
	 * @param object $connector The connector to be used for datasource connection
	 * @param object $log The log object to use for error logging
	 * @param object $action [optional] The action to be performed by the constructor ('load', 'new', 'update')
	 * @param object $actionparam [optional] An associative array for either loading or filling fields
	 */
	public function __construct($connector, $log, $action = '', $actionparam = null)
	{
		$this->Connector = $connector;
		$this->Log = $log;
		
		//-- Create events --
		$this->addEvents(array(
			'preload', 'postload',
			'presave', 'postsave',
			'preupdate', 'postupdate',
			'preremove', 'postremove'
		));
		
		switch ($action) {
			case 'load' : $this->load($actionparam); return;
			case 'new' : $this->Data = $actionparam; return;
			case 'update' : $this->update($actionparam); return;
			case 'remove' : $this->remove($actionparam['key']); return;
		}
	}
	
	/**
	 * Filters all fieldvalues
	 */
	public function filter()
	{
		$loc = basename(__FILE__).'/'.__METHOD__;		
		if (!class_exists('WpFilterer')) {
			$this->Log->logError($loc, '', 'Class missing', 'Class WPFilterer is missing but necessary in order to use the filter method.', self::$_espc.'009');
			die('Unexpected error. Script stopped. This incident has been reported.');
		}
		
		$ret = array();
		
		$af = $this->getFields();
		foreach ($af as $f) {
			$fv = $this->get($f);
			$fi = $this->getFilter($f);
			
			if (is_null($fi) || $fi === '') {
				continue;
			}
			
			if (strpos($fi, ',') !== false) {
				$afi = explode(',', $fi);
			} else {
				$afi = array($fi);
			}
			
			foreach ($afi as $filter) {
				$fvf = WpFilterer::filter($filter, $fv); 
				$fv = $fvf;
			}
			$this->set($f, $fvf);
		}
	}
	
	/**
	 * Retrieves the value of the specified fieldname
	 * @param string $fieldname The fieldname to retrieve the value of
	 * @return mixed The value of the field
	 */
	public function get($fieldname)
	{
		if (array_key_exists($fieldname, $this->Data)) {
			return $this->Data[$fieldname];
		}
		
		return null;
	}
	
	/**
	 * Retrieves the name of the table to use
	 * @abstract
	 * @return 
	 */
	public abstract function getTable();
	
	/**
	 * Retrieves an array of fields of the table
	 * @return array An array of fields
	 */
	public abstract function getFields();
	
	/**
	 * Retrieves the type of the field specified by name
	 * @param string $fieldname The name of the field to retrieve the type of
	 * @return string The type of the field. One of the following: string, number
	 */
	public abstract function getFieldType($fieldname);
	
	/**
	 * Retrieves the name of the filter for the field specified by name. Retrieves null or "", if no
	 * filter was specified for the given field.
	 * @param string $fieldname The name of the field to retrieve the filter of.
	 * @return string The name of the filter to be used for this field
	 */
	public abstract function getFilter($fieldname);
	
	/**
	 * Retrieves the name of the field which is used as primary key
	 * @return string The primary key field
	 */
	public abstract function getPkField();
	
	/**
	 * Retrieves the validation type for the field specfied by name. Can be null or "" if field does 
	 * not have a validation type.
	 * @param $fieldname The name of the field to retrieve the vtype of
	 * @return string The vtype of the field.
	 */
	public abstract function getVType($fieldname);
	
	/**
	 * Loads the dataset specified by 
	 * @param object $condition An associative array with fieldnames and values to be used for the query condition
	 * @return bool True if successfully loaded, false else
	 */
	public function load($condition)
	{
		$loc = basename(__FILE__).'/'.__METHOD__;
		if ($this->fireEvent('preload', array($this, $condition)) === false) {
			return false;
		}
		$this->reset();
		
		if (is_null($this->Connector)) {
			$this->Log->logError($loc, '', 'No connector', 'No connector was sepcified.', self::$_espc.'000');
			return false;
		}
		
		$c = '';
		foreach ($condition as $field => $value) {
			if ($this->getFieldType($field) === 'string') {
				$c .= $field.' = \''.$value.'\' AND ';
			} else {
				$c .= $field.' = '.$value.' AND ';
			}
		}
		$c = 'WHERE '.substr($c, 0, strlen($c)-5);
		
		if (!$this->Connector->select('*', $this->getTable(), $c)) {
			$this->Log->logError($loc, '', 'Error in method', 'The method WPConnect->select did not finish successfully.', self::$_espc.'001');
			return false;
		}		
		$result = $this->Connector->getResult('assocs');
		
		//-- Check result --
		if (is_null($result) || !is_array($result)) {
			$this->Log->logError($loc, '', 'Invalid result', 'The connector returned an invalid result. Array expected.', self::$_espc.'002');
			return false;
		}
		if (count($result) > 1) {
			$this->Log->logError($loc, '', 'Too many matches', 'There are more than one dataset matching the specified criteria. '.
				'SimpleDatexS can be used to handle single datasets only.', self::$_espc.'000');
			return false;
		}
		if (count($result) < 1) {
			return false;
		}
		
		//-- Prepare and return the object --
		
		$this->Data = $result[0];
		$this->Loaded = true;
		$this->_isNew = false;
		
		$this->fireEvent('postload', array($this));
		return true;
	}
	
	/**
	 * A quick set function to set multiple fields of the dataset with one call.
	 * 
	 * <code>
	 * $sd->qSet(array(
	 * 		'field1' => 'fieldvalue1',
	 * 		'field2' => 'fieldvalue2'
	 * ));
	 * </code>
	 * 
	 * @param array $fieldvalues An associative array of fieldnames and values to be set
	 */
	public function qSet($fieldvalues)
	{
		foreach ($fieldvalues as $field => $value) {
			$this->Data[$field] = $value;
		}
	}
	
	/**
	 * Removes the current dataset from datasource. If a key is provided, it removes the dataset specified by this 
	 * primary key instead of the currently loaded. This object will be resetted after removal.
	 * @param mixed $key [optional] The key of the dataset to remove. Can be skipped if the currently loaded dataset
	 * should be removed.
	 * @return bool True if successfully removed, false else
	 */
	public function remove($key = null)
	{
		$loc = basename(__FILE__).'/'.__METHOD__;
		if ($this->fireEvent('preremove', array($this)) === false) {
			return false;
		}
		
		if (is_null($this->Connector)) {
			$this->Log->logError($loc, '', 'No connector', 'No connector was sepcified.', self::$_espc.'003');
			return false;
		}
		
		if ($this->_isNew === true && is_null($key)) {
			unset($this);
			return true;
		}
		
		//-- Build condition --
		$pkf = $this->getPkField();
		if (is_null($key)) {
			$key = $this->get($pkf);
		}
		if ($this->getFieldType($pkf) === 'number') {
			$c = 'WHERE '.$pkf.' = '.strval($key);
		} else {
			$c = 'WHERE '.$pkf.' = \''.$key.'\'';
		}
		
		if (!$this->Connector->delete($this->getTable(), $c)) {
			$this->Log->logError($loc, '', 'Error in method', 'Method WPConnect->delete did not finish successfully.', self::$_espc.'004');
			return false;
		}
		
		$this->reset();
		$this->fireEvent('postremove', array($this));
		return true;
	}
	
	/**
	 * Resets the object as it was after construction
	 */
	public function reset()
	{
		$this->Data = array();
		$this->_isNew = true;
		$this->Loaded = false;
	}
	
	/**
	 * Saves the current dataset to datasource. If it is a new dataset it will do an insert statement, otherwise 
	 * an update statement.
	 * @return bool True if successfully saved, false else
	 */
	public function save()
	{
		$loc = basename(__FILE__).'/'.__METHOD__;
		if ($this->fireEvent('presave', array($this)) === false) {
			return false;
		}
		$blnCheck = false;
		
		if (is_null($this->Connector)) {
			$this->Log->logError($loc, '', 'No connector', 'No connector was sepcified.', self::$_espc.'005');
			return false;
		}
		
		$this->filter();
		
		//-- Build condition --
		$pkf = $this->getPkField();
		$key = $this->get($pkf);
		if ($this->getFieldType($pkf) === 'number') {
			$c = 'WHERE '.$pkf.' = '.strval($key);
		} else {
			$c = 'WHERE '.$pkf.' = \''.$key.'\'';
		}
		
		if ($this->_isNew === true) {
			$m = 'insert';
			$blnCheck = $this->Connector->insert($this->getTable(), $this->Data);
			
			//-- Fill auto-increment value --
			if (is_null($this->get($this->getPkField())) && !is_null($this->Connector->getInsertId())) {
				$this->set($this->getPkField(), $this->Connector->getInsertId());
			}
			
		} else {
			$m = 'update';
			if (is_null($key)) {
				$this->Log->logError($loc, '', 'Invalid primary key', 'Primary key was expected to be set but is null. Update not possible.', self::$_espc.'008');
				return false;
			}
			$blnCheck = $this->Connector->update($this->getTable(), $this->Data, $c);
		}
		
		if ($blnCheck !== true) {
			$this->Log->logError($loc, '', 'Error in method', 'Method WPConnect->'.$m.' did not finish successfully.', self::$_espc.'006');
			return false;
		}
		
		$this->_isNew = false;
		$this->fireEvent('postsave', array($this));
		return true;
	}
	
	/**
	 * Set the value of a field
	 * @param string $fieldname The name of the field to set
	 * @param mixed $value The value to set the field to
	 */
	public function set($fieldname, $value)
	{
		$this->Data[$fieldname] = $value;
	}
	
	/**
	 * Peforms an update request without loading the data before, for best performance. The method performs a reset call 
	 * on start and end.
	 * @param array $fieldvalues An associative array of all the fieldvalues to update. This array must also include the
	 * primary key. The primary key cannot be updated using this method. This method will not check wether a dataset with
	 * the given primary key exists in database.
	 * @return bool True if successfully updated, false else
	 */
	public function update($fieldvalues)
	{
		$loc = basename(__FILE__).'/'.__METHOD__;
		if ($this->fireEvent('preupdate', array($this,$fieldvalues)) === false) {
			return false;
		}
		$this->reset();
		$this->_isNew = false;
		$this->Data = $fieldvalues;
		
		$ret = $this->save();
		$this->reset();
		
		if ($ret !== true) {
			$this->Log->logError($loc, '', 'Error in method', 'Method save did not finish successfully.', self::$_espc.'007');
		} else {
			$this->fireEvent('postupdate', array($this));
		}
		
		return $ret;
	}
	
	/**
	 * Validates all the fields and returns true if all fields pass the validation or an array of fields that 
	 * did not pass the validation
	 * @return mixed True if all fields are valid or an array of invalid fields
	 */
	public function validate()
	{
		$loc = basename(__FILE__).'/'.__METHOD__;		
		if (!class_exists('WpValidator')) {
			$this->Log->logError($loc, '', 'Class missing', 'Class WPValidator is missing but necessary in order to use the validate method.', self::$_espc.'008');
		}
		
		$ret = array();
		
		$af = $this->getFields();
		foreach ($af as $f) {
			$fv = $this->get($f);
			
			//-- Check type --
			$ft = $this->getFieldType($f);
			if (!is_null($fv)) {
				if ($ft === 'string' && !is_string($fv)) {
					$ret[] = $f;
				}
				if ($ft === 'number' && !is_numeric($fv)) {
					$ret[] = $f;
				}
			}
			
			//-- Use validator --
			$fvt = $this->getVType($f);
			if (!is_null($fvt) && $fvt !== '') {
				if (!class_exists('WpValidator')) {
					$ret[] = $f;
				} else {
					if (WpValidator::validate($fvt, $fv) !== true) {
						$ret[] = $f;
					}
				}
			}
		}
		
		if (count($ret) > 0) {
			return $ret;
		}
		
		return true;
	}
}

/* End of file SimpleDatexS.php */
/* Location: ./wildpack/dataaccess/wpdatex/SimpleDatexS.php */ 